/**
 *
 * The MIT License
 *
 * Copyright (c) 2008 Board of Trustees, Leland Stanford Jr. University. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.macdadi.core.client.auth;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Cookies;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import org.macdadi.core.client.ui.GeneralHyperlink;

import java.util.Date;

/**
 * @author Ben Suter <ben.suter@gmail.com>
 */
/**
 * This widget displays the name of the currently authenticated
 * user, as well as a "logout" link. If no user is authenticated, 
 * it displays an indication of such. It also provides methods to
 * display a pop-up login dialog. When not authenticated, a "login"
 * link is displayed.
 *
 * When created, this control will look for client-side cookies saved
 * in the browser that indicate the presence of a previously active session.
 * If such session identifiers are found, it will attempt to rejoin this
 * session - if the session is no longer active, this attempt will fail.
 * In practice this facilitates moving between multiple projects, without
 * the user being asked to login multiple times.
 *
 * Example usage pattern:
 * 1. In the main view, maintain a reference to an AuthenticationControl instance
 * 2. Add the AC to the view
 * 3. Add a listener using AC.setListener(...):
 *     userAuthenticated: populate views with domain
 *     userLoggedOut: clear domain, clear views, AC.promptLogin
 * 4. After GUI has been built, AC.promptLogin
 *
 * @author Ben Suter <ben.suter@gmail.com>
 */
public class AuthenticationControl extends Composite {
    protected AuthenticatedUser authenticatedUser;
    protected HorizontalPanel panel;
    protected Label username;
    protected GeneralHyperlink logout;
    protected GeneralHyperlink login;
    protected AuthenticationListener listener;
    protected LoginDialog loginDialog;

    protected boolean promptLoginOnLogout = false;
    
    protected static final String NOT_AUTHENTICATED = "";

    public static final String CSS_CLASS = "macdadi-AuthenticationDisplay";
    public static final String AUTH_TOKEN_COOKIE = "authToken";
    public static final String AUTH_EMAIL = "authEmail";

    public AuthenticationControl() {
        setupView();
        loginDialog = new LoginDialog(this);
        checkForValidSessionCookie(true, false);  // login from session cookie if still valid
    }

    protected void setupView()
    {
        panel = new HorizontalPanel();

        /* Space between name and logout link */
        panel.setSpacing(10);
        
        username = new Label();
        username.setText(NOT_AUTHENTICATED);
        panel.add(username);

        login = new GeneralHyperlink();
        login.setTargetHistoryToken(""); /* Do not effect browser history */
        login.setText("Login");
        panel.add(login);
        login.addClickListener(new ClickListener() {
            public void onClick(Widget arg0) {
                promptLogin();
            }
        });
        login.setVisible(true); 

        logout = new GeneralHyperlink();
        logout.setTargetHistoryToken(""); /* Do not effect browser history */
        logout.setText("Logout");
        panel.add(logout);        
        logout.addClickListener(new ClickListener() {
            public void onClick(Widget arg0) {
                doLogout();
            }           
        });
        logout.setVisible(false);
                
        initWidget(panel);
        setStyleName(CSS_CLASS);
    }

    protected void doLogout() {
        if ( authenticatedUser == null )
            return;

        AuthService.App.getInstance().logout(authenticatedUser, new AsyncCallback() {
            public void onFailure(Throwable caught) {
                GWT.log("Failed to logout user", caught);
            }

            public void onSuccess(Object result) {
                boolean success = ((Boolean) result).booleanValue();
                
                if ( ! success ) {
                    GWT.log("Failed to logout user", null);
                    checkForValidSessionCookie(true, true); // in case logged out in a different window 
                } else {
                    logoutSucceeded();
                }
            }
        });
    }

    protected void logoutSucceeded()
    {
        authenticatedUser = null;
        username.setText(NOT_AUTHENTICATED);
        login.setVisible(true);
        logout.setVisible(false);
        if (listener != null)
            listener.userLoggedOut();

        Cookies.removeCookie(AUTH_TOKEN_COOKIE);
        Cookies.removeCookie(AUTH_EMAIL);

        if (promptLoginOnLogout)
            promptLogin();
    }

    protected void authenticationSucceeded(AuthenticatedUser user) {
        authenticatedUser = user;
        username.setText(authenticatedUser.getName() + " (" + authenticatedUser.getEmail() + ")");
        login.setVisible(false);
        logout.setVisible(true);
        if ( listener != null )
            listener.userAuthenticated(authenticatedUser);

        /* Remember the session id as a client-side cookie for one hour */
        Date expires = new Date(System.currentTimeMillis() + 1000*60*60);
        Cookies.removeCookie(AUTH_TOKEN_COOKIE); // Is this necessary, given the next setCookie() call ?
        Cookies.removeCookie(AUTH_EMAIL);
        Cookies.setCookie(AUTH_TOKEN_COOKIE, user.getAuthToken(), expires, null, "/", false);
        Cookies.setCookie(AUTH_EMAIL, user.getEmail(), expires, null, "/", false);
    }

    public void checkForValidSessionCookie(final boolean authSucceededOnSuccess, final boolean logoutSucceededOnFailure)
    {
        String authToken;
        authToken = Cookies.getCookie(AUTH_TOKEN_COOKIE);
        if ( authToken == null )
            return;

        String email;
        email = Cookies.getCookie(AUTH_EMAIL);
        if ( email == null )
            return;

        /* We found a cookie with some previous session id, so now check wether it is
         * is still valid. If it is valid, we auto-login the user, but if not, we
         * do nothing at all.
         */
        AuthService.App.getInstance().getUserForSession(email, authToken, new AsyncCallback()
        {
            public void onSuccess(Object result)
            {
                AuthenticatedUser user = (AuthenticatedUser) result;

                if (user == null) {
                    GWT.log("The cookie-stored session is not valid", null);
                    if ( logoutSucceededOnFailure )
                        logoutSucceeded();
                } else {
                    if ( authSucceededOnSuccess )
                        authenticationSucceeded(user);
                }
            }

            public void onFailure(Throwable caught)
            {
                // TODO: handle error
                GWT.log("Unable to check whether the cookie-stored session is valid", caught);
            }
        });

    }
    
    
    /* API */        
    public void promptLogin() {
        loginDialog.show();
    }

    public void reloadAuthenticatedUser() 
    {
        if ( ! isAuthenticated() ) {
            return;
        }

        /* User is currently logged in, but we want to reload in case user
         * meta-data has changed.
         */
         AuthService.App.getInstance().getUserForSession(authenticatedUser.getEmail(), authenticatedUser.getAuthToken(), new AsyncCallback()
        {
            public void onSuccess(Object result)
            {
                AuthenticatedUser user = (AuthenticatedUser) result;

                if (user == null) {
                    logoutSucceeded();
                } else {
                    authenticationSucceeded(user);
                }
            }

            public void onFailure(Throwable caught)
            {
                // TODO: handle error
                GWT.log("Unable to reload the current user", caught);
            }
        });
    }
        
    public AuthenticatedUser getAuthenticatedUser() {
        return authenticatedUser;
    }
    
    public boolean isAuthenticated() {
        return ( authenticatedUser != null );
    }
    
    public void setListener(AuthenticationListener listener) {
        this.listener = listener;
    }
        
    public interface AuthenticationListener {
        public void userAuthenticated(AuthenticatedUser user);
        public void userLoggedOut();
    }

    /**
     * Add a link alongside the "Login", "Logout" controls.
     * @param text
     * @param href
     * @param whichSide  SIDE_RIGHT or SIDE_LEFT
     */
    public void addLink(String text, String href, int whichSide)
    {
        HTML link = new HTML("<a href='" + href + "'>" + text + "</a>");
        if ( whichSide == 1 )
            panel.insert(link, 0);
        else
            panel.add(link);
    }
    public static final int SIDE_LEFT = 1;
    public static final int SIDE_RIGHT = 2;
    
    /**
     * LoginDialog is a modal dialog for entering a username
     * and password and authenticating these credentials.
     */
    protected class LoginDialog extends DialogBox {

        protected AuthenticationControl authDisplay;
        protected TextBox nameInput;
        protected PasswordTextBox passwordInput;

        public LoginDialog(AuthenticationControl authDisplay) {
            super(true, true);  /* Allow user to dismiss the dialog */
            this.authDisplay = authDisplay;
            setupDialog();
        }

        private void setupDialog() {
            setText("Please login");

            VerticalPanel loginForm = new VerticalPanel();

            loginForm.add(new Label("Email:"));
            nameInput = new TextBox();
            loginForm.add(nameInput);

            loginForm.add(new Label("Password:"));
            passwordInput = new PasswordTextBox();
            loginForm.add(passwordInput);

            Button submit = new Button("Submit");
            loginForm.add(submit);
            submit.addClickListener(new ClickListener() {

                public void onClick(Widget arg0) {
                    if (validate()) {
                        authenticate();
                    }
                }
            });

            setWidget(loginForm);
        }

        public void show() {
            int x, y;
//            x = 200;
//            y = 150;
            x = authDisplay.getAbsoluteLeft();
            y = authDisplay.getAbsoluteTop();
            this.setPopupPosition(x, y);
            super.show();
        }

        protected boolean validate() {
            // TODO: validate input and provide message or hide message
            return true;
        }

        protected void authenticate() {
            String name, password;
            name = nameInput.getText().trim();
            password = passwordInput.getText().trim();

            AuthService.App.getInstance().login(name, password, new AsyncCallback() {
                public void onFailure(Throwable caught) {
                    GWT.log("Failed to authenticate user", caught);
                }

                public void onSuccess(Object result) {
                    if ( result == null ) {
                        GWT.log("Failed to authenticate user", null);
                        return;
                    }
                    
                    hide();
                    passwordInput.setText("");
                    /* Update the AuthenticationDisplay */
                    authDisplay.authenticationSucceeded((AuthenticatedUser) result);
                }
            });
        }
    }
}
